/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id: ExsltDatetime.java,v 1.2.4.1 2005/09/10 18:50:49 jeffsuttor Exp $
 */

package com.sun.org.apache.xalan.internal.lib;


import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import com.sun.org.apache.xpath.internal.objects.XBoolean;
import com.sun.org.apache.xpath.internal.objects.XNumber;
import com.sun.org.apache.xpath.internal.objects.XObject;

/**
 * This class contains EXSLT dates and times extension functions.
 * It is accessed by specifying a namespace URI as follows:
 * <pre>
 *    xmlns:datetime="http://exslt.org/dates-and-times"
 * </pre>
 *
 * The documentation for each function has been copied from the relevant
 * EXSLT Implementer page.
 *
 * @xsl.usage general
 * @see <a href="http://www.exslt.org/">EXSLT</a>
 */

public class ExsltDatetime {

  // Datetime formats (era and zone handled separately).
  static final String dt = "yyyy-MM-dd'T'HH:mm:ss";
  static final String d = "yyyy-MM-dd";
  static final String gym = "yyyy-MM";
  static final String gy = "yyyy";
  static final String gmd = "--MM-dd";
  static final String gm = "--MM--";
  static final String gd = "---dd";
  static final String t = "HH:mm:ss";
  static final String EMPTY_STR = "";

  /**
   * The date:date-time function returns the current date and time as a date/time string.
   * The date/time string that's returned must be a string in the format defined as the
   * lexical representation of xs:dateTime in
   * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
   * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
   * The date/time string format must include a time zone, either a Z to indicate Coordinated
   * Universal Time or a + or - followed by the difference between the difference from UTC
   * represented as hh:mm.
   */
  public static String dateTime() {
    Calendar cal = Calendar.getInstance();
    Date datetime = cal.getTime();
    // Format for date and time.
    SimpleDateFormat dateFormat = new SimpleDateFormat(dt);

    StringBuffer buff = new StringBuffer(dateFormat.format(datetime));
    // Must also include offset from UTF.
    // Get the offset (in milliseconds).
    int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
    // If there is no offset, we have "Coordinated
    // Universal Time."
    if (offset == 0) {
      buff.append("Z");
    } else {
      // Convert milliseconds to hours and minutes
      int hrs = offset / (60 * 60 * 1000);
      // In a few cases, the time zone may be +/-hh:30.
      int min = offset % (60 * 60 * 1000);
      char posneg = hrs < 0 ? '-' : '+';
      buff.append(posneg).append(formatDigits(hrs)).append(':').append(formatDigits(min));
    }
    return buff.toString();
  }

  /**
   * Represent the hours and minutes with two-digit strings.
   *
   * @param q hrs or minutes.
   * @return two-digit String representation of hrs or minutes.
   */
  private static String formatDigits(int q) {
    String dd = String.valueOf(Math.abs(q));
    return dd.length() == 1 ? '0' + dd : dd;
  }

  /**
   * The date:date function returns the date specified in the date/time string given
   * as the argument. If no argument is given, then the current local date/time, as
   * returned by date:date-time is used as a default argument.
   * The date/time string that's returned must be a string in the format defined as the
   * lexical representation of xs:dateTime in
   * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * If the argument is not in either of these formats, date:date returns an empty string ('').
   * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
   * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
   * The date is returned as a string with a lexical representation as defined for xs:date in
   * [3.2.9 date] of [XML Schema Part 2: Datatypes]. The date format is basically CCYY-MM-DD,
   * although implementers should consult [XML Schema Part 2: Datatypes] and [ISO 8601] for details.
   * If no argument is given or the argument date/time specifies a time zone, then the date string
   * format must include a time zone, either a Z to indicate Coordinated Universal Time or a + or -
   * followed by the difference between the difference from UTC represented as hh:mm. If an argument
   * is specified and it does not specify a time zone, then the date string format must not include
   * a time zone.
   */
  public static String date(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String leader = edz[0];
    String datetime = edz[1];
    String zone = edz[2];
    if (datetime == null || zone == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d};
    String formatOut = d;
    Date date = testFormats(datetime, formatsIn);
    if (date == null) {
      return EMPTY_STR;
    }

    SimpleDateFormat dateFormat = new SimpleDateFormat(formatOut);
    dateFormat.setLenient(false);
    String dateOut = dateFormat.format(date);
    if (dateOut.length() == 0) {
      return EMPTY_STR;
    } else {
      return (leader + dateOut + zone);
    }
  }


  /**
   * See above.
   */
  public static String date() {
    String datetime = dateTime().toString();
    String date = datetime.substring(0, datetime.indexOf("T"));
    String zone = datetime.substring(getZoneStart(datetime));
    return (date + zone);
  }

  /**
   * The date:time function returns the time specified in the date/time string given as the
   * argument. If no argument is given, then the current local date/time, as returned by
   * date:date-time is used as a default argument. The date/time string that's returned must be a
   * string in the format defined as the lexical representation of xs:dateTime in <a
   * href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of <a
   * href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>. If the argument
   * string is not in this format, date:time returns an empty string (''). The date/time format is
   * basically CCYY-MM-DDThh:mm:ss, although implementers should consult <a
   * href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and <a
   * href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details. The date is returned as a
   * string with a lexical representation as defined for xs:time in <a
   * href="http://www.w3.org/TR/xmlschema-2/#time">[3.2.8 time]</a> of [XML Schema Part 2:
   * Datatypes]. The time format is basically hh:mm:ss, although implementers should consult [XML
   * Schema Part 2: Datatypes] and [ISO 8601] for details. If no argument is given or the argument
   * date/time specifies a time zone, then the time string format must include a time zone, either a
   * Z to indicate Coordinated Universal Time or a + or - followed by the difference between the
   * difference from UTC represented as hh:mm. If an argument is specified and it does not specify a
   * time zone, then the time string format must not include a time zone.
   */
  public static String time(String timeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(timeIn);
    String time = edz[1];
    String zone = edz[2];
    if (time == null || zone == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d, t};
    String formatOut = t;
    Date date = testFormats(time, formatsIn);
    if (date == null) {
      return EMPTY_STR;
    }
    SimpleDateFormat dateFormat = new SimpleDateFormat(formatOut);
    String out = dateFormat.format(date);
    return (out + zone);
  }

  /**
   * See above.
   */
  public static String time() {
    String datetime = dateTime().toString();
    String time = datetime.substring(datetime.indexOf("T") + 1);

    // The datetime() function returns the zone on the datetime string.  If we
    // append it, we get the zone substring duplicated.
    // Fix for JIRA 2013

    // String zone = datetime.substring(getZoneStart(datetime));
    // return (time + zone);
    return (time);
  }

  /**
   * The date:year function returns the year of a date as a number. If no
   * argument is given, then the current local date/time, as returned by
   * date:date-time is used as a default argument.
   * The date/time string specified as the first argument must be a right-truncated
   * string in the format defined as the lexical representation of xs:dateTime in one
   * of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gYearMonth (CCYY-MM)
   * xs:gYear (CCYY)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double year(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    boolean ad = edz[0].length() == 0; // AD (Common Era -- empty leader)
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d, gym, gy};
    double yr = getNumber(datetime, formats, Calendar.YEAR);
    if (ad || yr == Double.NaN) {
      return yr;
    } else {
      return -yr;
    }
  }

  /**
   * See above.
   */
  public static double year() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.YEAR);
  }

  /**
   * The date:month-in-year function returns the month of a date as a number. If no argument
   * is given, then the current local date/time, as returned by date:date-time is used
   * as a default argument.
   * The date/time string specified as the first argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime in one of
   * the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gYearMonth (CCYY-MM)
   * xs:gMonth (--MM--)
   * xs:gMonthDay (--MM-DD)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double monthInYear(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d, gym, gm, gmd};
    return getNumber(datetime, formats, Calendar.MONTH) + 1;
  }

  /**
   * See above.
   */
  public static double monthInYear() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.MONTH) + 1;
  }

  /**
   * The date:week-in-year function returns the week of the year as a number. If no argument
   * is given, then the current local date/time, as returned by date:date-time is used as the
   * default argument. For the purposes of numbering, counting follows ISO 8601: week 1 in a year
   * is the week containing the first Thursday of the year, with new weeks beginning on a Monday.
   * The date/time string specified as the argument is a right-truncated string in the format
   * defined as the lexical representation of xs:dateTime in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>. The
   * permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double weekInYear(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d};
    return getNumber(datetime, formats, Calendar.WEEK_OF_YEAR);
  }

  /**
   * See above.
   */
  public static double weekInYear() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.WEEK_OF_YEAR);
  }

  /**
   * The date:day-in-year function returns the day of a date in a year
   * as a number. If no argument is given, then the current local
   * date/time, as returned by date:date-time is used the default argument.
   * The date/time string specified as the argument is a right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double dayInYear(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d};
    return getNumber(datetime, formats, Calendar.DAY_OF_YEAR);
  }

  /**
   * See above.
   */
  public static double dayInYear() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.DAY_OF_YEAR);
  }


  /**
   * The date:day-in-month function returns the day of a date as a number.
   * If no argument is given, then the current local date/time, as returned
   * by date:date-time is used the default argument.
   * The date/time string specified as the argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gMonthDay (--MM-DD)
   * xs:gDay (---DD)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double dayInMonth(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    String[] formats = {dt, d, gmd, gd};
    double day = getNumber(datetime, formats, Calendar.DAY_OF_MONTH);
    return day;
  }

  /**
   * See above.
   */
  public static double dayInMonth() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.DAY_OF_MONTH);
  }

  /**
   * The date:day-of-week-in-month function returns the day-of-the-week
   * in a month of a date as a number (e.g. 3 for the 3rd Tuesday in May).
   * If no argument is given, then the current local date/time, as returned
   * by date:date-time is used the default argument.
   * The date/time string specified as the argument is a right-truncated string
   * in the format defined as the lexical representation of xs:dateTime in one
   * of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double dayOfWeekInMonth(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d};
    return getNumber(datetime, formats, Calendar.DAY_OF_WEEK_IN_MONTH);
  }

  /**
   * See above.
   */
  public static double dayOfWeekInMonth() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
  }


  /**
   * The date:day-in-week function returns the day of the week given in a date as a number. If no
   * argument is given, then the current local date/time, as returned by date:date-time is used the
   * default argument. The date/time string specified as the argument is a right-truncated string in
   * the format defined as the lexical representation of xs:dateTime in one of the formats defined
   * in <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>. The
   * permitted formats are as follows: xs:dateTime (CCYY-MM-DDThh:mm:ss) xs:date (CCYY-MM-DD) If the
   * date/time string is not in one of these formats, then NaN is returned. The numbering of days of
   * the week starts at 1 for Sunday, 2 for Monday and so on up to 7 for Saturday.
   */
  public static double dayInWeek(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, d};
    return getNumber(datetime, formats, Calendar.DAY_OF_WEEK);
  }

  /**
   * See above.
   */
  public static double dayInWeek() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.DAY_OF_WEEK);
  }

  /**
   * The date:hour-in-day function returns the hour of the day as a number.
   * If no argument is given, then the current local date/time, as returned
   * by date:date-time is used the default argument.
   * The date/time string specified as the argument is a right-truncated
   * string  in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:time (hh:mm:ss)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double hourInDay(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, t};
    return getNumber(datetime, formats, Calendar.HOUR_OF_DAY);
  }

  /**
   * See above.
   */
  public static double hourInDay() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.HOUR_OF_DAY);
  }

  /**
   * The date:minute-in-hour function returns the minute of the hour
   * as a number. If no argument is given, then the current local
   * date/time, as returned by date:date-time is used the default argument.
   * The date/time string specified as the argument is a right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:time (hh:mm:ss)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double minuteInHour(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, t};
    return getNumber(datetime, formats, Calendar.MINUTE);
  }

  /**
   * See above.
   */
  public static double minuteInHour() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.MINUTE);
  }

  /**
   * The date:second-in-minute function returns the second of the minute
   * as a number. If no argument is given, then the current local
   * date/time, as returned by date:date-time is used the default argument.
   * The date/time string specified as the argument is a right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:time (hh:mm:ss)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static double secondInMinute(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return Double.NaN;
    }

    String[] formats = {dt, t};
    return getNumber(datetime, formats, Calendar.SECOND);
  }

  /**
   * See above.
   */
  public static double secondInMinute() {
    Calendar cal = Calendar.getInstance();
    return cal.get(Calendar.SECOND);
  }

  /**
   * The date:leap-year function returns true if the year given in a date
   * is a leap year. If no argument is given, then the current local
   * date/time, as returned by date:date-time is used as a default argument.
   * The date/time string specified as the first argument must be a
   * right-truncated string in the format defined as the lexical representation
   * of xs:dateTime in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gYearMonth (CCYY-MM)
   * xs:gYear (CCYY)
   * If the date/time string is not in one of these formats, then NaN is returned.
   */
  public static XObject leapYear(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return new XNumber(Double.NaN);
    }

    String[] formats = {dt, d, gym, gy};
    double dbl = getNumber(datetime, formats, Calendar.YEAR);
    if (dbl == Double.NaN) {
      return new XNumber(Double.NaN);
    }
    int yr = (int) dbl;
    return new XBoolean(yr % 400 == 0 || (yr % 100 != 0 && yr % 4 == 0));
  }

  /**
   * See above.
   */
  public static boolean leapYear() {
    Calendar cal = Calendar.getInstance();
    int yr = (int) cal.get(Calendar.YEAR);
    return (yr % 400 == 0 || (yr % 100 != 0 && yr % 4 == 0));
  }

  /**
   * The date:month-name function returns the full name of the month of a date.
   * If no argument is given, then the current local date/time, as returned by
   * date:date-time is used the default argument.
   * The date/time string specified as the argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime in
   * one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gYearMonth (CCYY-MM)
   * xs:gMonth (--MM--)
   * If the date/time string is not in one of these formats, then an empty string ('')
   * is returned.
   * The result is an English month name: one of 'January', 'February', 'March',
   * 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November'
   * or 'December'.
   */
  public static String monthName(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d, gym, gm};
    String formatOut = "MMMM";
    return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
  }

  /**
   * See above.
   */
  public static String monthName() {
    Calendar cal = Calendar.getInstance();
    String format = "MMMM";
    return getNameOrAbbrev(format);
  }

  /**
   * The date:month-abbreviation function returns the abbreviation of the month of
   * a date. If no argument is given, then the current local date/time, as returned
   * by date:date-time is used the default argument.
   * The date/time string specified as the argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime in
   * one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * xs:gYearMonth (CCYY-MM)
   * xs:gMonth (--MM--)
   * If the date/time string is not in one of these formats, then an empty string ('')
   * is returned.
   * The result is a three-letter English month abbreviation: one of 'Jan', 'Feb', 'Mar',
   * 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov' or 'Dec'.
   * An implementation of this extension function in the EXSLT date namespace must conform
   * to the behaviour described in this document.
   */
  public static String monthAbbreviation(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d, gym, gm};
    String formatOut = "MMM";
    return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
  }

  /**
   * See above.
   */
  public static String monthAbbreviation() {
    String format = "MMM";
    return getNameOrAbbrev(format);
  }

  /**
   * The date:day-name function returns the full name of the day of the week
   * of a date.  If no argument is given, then the current local date/time,
   * as returned by date:date-time is used the default argument.
   * The date/time string specified as the argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * If the date/time string is not in one of these formats, then the empty string ('')
   * is returned.
   * The result is an English day name: one of 'Sunday', 'Monday', 'Tuesday', 'Wednesday',
   * 'Thursday' or 'Friday'.
   * An implementation of this extension function in the EXSLT date namespace must conform
   * to the behaviour described in this document.
   */
  public static String dayName(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d};
    String formatOut = "EEEE";
    return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
  }

  /**
   * See above.
   */
  public static String dayName() {
    String format = "EEEE";
    return getNameOrAbbrev(format);
  }

  /**
   * The date:day-abbreviation function returns the abbreviation of the day
   * of the week of a date. If no argument is given, then the current local
   * date/time, as returned  by date:date-time is used the default argument.
   * The date/time string specified as the argument is a left or right-truncated
   * string in the format defined as the lexical representation of xs:dateTime
   * in one of the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * xs:date (CCYY-MM-DD)
   * If the date/time string is not in one of these formats, then the empty string
   * ('') is returned.
   * The result is a three-letter English day abbreviation: one of 'Sun', 'Mon', 'Tue',
   * 'Wed', 'Thu' or 'Fri'.
   * An implementation of this extension function in the EXSLT date namespace must conform
   * to the behaviour described in this document.
   */
  public static String dayAbbreviation(String datetimeIn)
      throws ParseException {
    String[] edz = getEraDatetimeZone(datetimeIn);
    String datetime = edz[1];
    if (datetime == null) {
      return EMPTY_STR;
    }

    String[] formatsIn = {dt, d};
    String formatOut = "EEE";
    return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
  }

  /**
   * See above.
   */
  public static String dayAbbreviation() {
    String format = "EEE";
    return getNameOrAbbrev(format);
  }

  /**
   * Returns an array with the 3 components that a datetime input string
   * may contain: - (for BC era), datetime, and zone. If the zone is not
   * valid, return null for that component.
   */
  private static String[] getEraDatetimeZone(String in) {
    String leader = "";
    String datetime = in;
    String zone = "";
    if (in.charAt(0) == '-' && !in.startsWith("--")) {
      leader = "-"; //  '+' is implicit , not allowed
      datetime = in.substring(1);
    }
    int z = getZoneStart(datetime);
    if (z > 0) {
      zone = datetime.substring(z);
      datetime = datetime.substring(0, z);
    } else if (z == -2) {
      zone = null;
    }
    //System.out.println("'" + leader + "' " + datetime + " " + zone);
    return new String[]{leader, datetime, zone};
  }

  /**
   * Get the start of zone information if the input ends
   * with 'Z' or +/-hh:mm. If a zone string is not
   * found, return -1; if the zone string is invalid,
   * return -2.
   */
  private static int getZoneStart(String datetime) {
    if (datetime.indexOf("Z") == datetime.length() - 1) {
      return datetime.length() - 1;
    } else if (datetime.length() >= 6
        && datetime.charAt(datetime.length() - 3) == ':'
        && (datetime.charAt(datetime.length() - 6) == '+'
        || datetime.charAt(datetime.length() - 6) == '-')) {
      try {
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm");
        dateFormat.setLenient(false);
        Date d = dateFormat.parse(datetime.substring(datetime.length() - 5));
        return datetime.length() - 6;
      } catch (ParseException pe) {
        System.out.println("ParseException " + pe.getErrorOffset());
        return -2; // Invalid.
      }

    }
    return -1; // No zone information.
  }

  /**
   * Attempt to parse an input string with the allowed formats, returning
   * null if none of the formats work.
   */
  private static Date testFormats(String in, String[] formats)
      throws ParseException {
    for (int i = 0; i < formats.length; i++) {
      try {
        SimpleDateFormat dateFormat = new SimpleDateFormat(formats[i]);
        dateFormat.setLenient(false);
        return dateFormat.parse(in);
      } catch (ParseException pe) {
      }
    }
    return null;
  }


  /**
   * Parse the input string and return the corresponding calendar field
   * number.
   */
  private static double getNumber(String in, String[] formats, int calField)
      throws ParseException {
    Calendar cal = Calendar.getInstance();
    cal.setLenient(false);
    // Try the allowed formats, from longest to shortest.
    Date date = testFormats(in, formats);
    if (date == null) {
      return Double.NaN;
    }
    cal.setTime(date);
    return cal.get(calField);
  }

  /**
   * Get the full name or abbreviation of the month or day.
   */
  private static String getNameOrAbbrev(String in,
      String[] formatsIn,
      String formatOut)
      throws ParseException {
    for (int i = 0; i < formatsIn.length; i++) // from longest to shortest.
    {
      try {
        SimpleDateFormat dateFormat = new SimpleDateFormat(formatsIn[i], Locale.ENGLISH);
        dateFormat.setLenient(false);
        Date dt = dateFormat.parse(in);
        dateFormat.applyPattern(formatOut);
        return dateFormat.format(dt);
      } catch (ParseException pe) {
      }
    }
    return "";
  }

  /**
   * Get the full name or abbreviation for the current month or day
   * (no input string).
   */
  private static String getNameOrAbbrev(String format) {
    Calendar cal = Calendar.getInstance();
    SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH);
    return dateFormat.format(cal.getTime());
  }

  /**
   * The date:format-date function formats a date/time according to a pattern.
   * <p>
   * The first argument to date:format-date specifies the date/time to be
   * formatted. It must be right or left-truncated date/time strings in one of
   * the formats defined in
   * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
   * The permitted formats are as follows:
   * <ul>
   * <li>xs:dateTime (CCYY-MM-DDThh:mm:ss)
   * <li>xs:date (CCYY-MM-DD)
   * <li>xs:time (hh:mm:ss)
   * <li>xs:gYearMonth (CCYY-MM)
   * <li>xs:gYear (CCYY)
   * <li>xs:gMonthDay (--MM-DD)
   * <li>xs:gMonth (--MM--)
   * <li>xs:gDay (---DD)
   * </ul>
   * The second argument is a string that gives the format pattern used to
   * format the date. The format pattern must be in the syntax specified by
   * the JDK 1.1 SimpleDateFormat class. The format pattern string is
   * interpreted as described for the JDK 1.1 SimpleDateFormat class.
   * <p>
   * If the date/time format is right-truncated (i.e. in a format other than
   * xs:time, or xs:dateTime) then any missing components are assumed to be as
   * follows: if no month is specified, it is given a month of 01; if no day
   * is specified, it is given a day of 01; if no time is specified, it is
   * given a time of 00:00:00.
   * <p>
   * If the date/time format is left-truncated (i.e. xs:time, xs:gMonthDay,
   * xs:gMonth or xs:gDay) and the format pattern has a token that uses a
   * component that is missing from the date/time format used, then that token
   * is replaced with an empty string ('') within the result.
   *
   * The author is Helg Bredow (helg.bredow@kalido.com)
   */
  public static String formatDate(String dateTime, String pattern) {
    final String yearSymbols = "Gy";
    final String monthSymbols = "M";
    final String daySymbols = "dDEFwW";
    TimeZone timeZone;
    String zone;

    // Get the timezone information if it was supplied and modify the
    // dateTime so that SimpleDateFormat will understand it.
    if (dateTime.endsWith("Z") || dateTime.endsWith("z")) {
      timeZone = TimeZone.getTimeZone("GMT");
      dateTime = dateTime.substring(0, dateTime.length() - 1) + "GMT";
      zone = "z";
    } else if ((dateTime.length() >= 6)
        && (dateTime.charAt(dateTime.length() - 3) == ':')
        && ((dateTime.charAt(dateTime.length() - 6) == '+')
        || (dateTime.charAt(dateTime.length() - 6) == '-'))) {
      String offset = dateTime.substring(dateTime.length() - 6);

      if ("+00:00".equals(offset) || "-00:00".equals(offset)) {
        timeZone = TimeZone.getTimeZone("GMT");
      } else {
        timeZone = TimeZone.getTimeZone("GMT" + offset);
      }
      zone = "z";
      // Need to adjust it since SimpleDateFormat requires GMT+hh:mm but
      // we have +hh:mm.
      dateTime = dateTime.substring(0, dateTime.length() - 6) + "GMT" + offset;
    } else {
      // Assume local time.
      timeZone = TimeZone.getDefault();
      zone = "";
      // Leave off the timezone since SimpleDateFormat will assume local
      // time if time zone is not included.
    }
    String[] formats = {dt + zone, d, gym, gy};

    // Try the time format first. We need to do this to prevent
    // SimpleDateFormat from interpreting a time as a year. i.e we just need
    // to check if it's a time before we check it's a year.
    try {
      SimpleDateFormat inFormat = new SimpleDateFormat(t + zone);
      inFormat.setLenient(false);
      Date d = inFormat.parse(dateTime);
      SimpleDateFormat outFormat = new SimpleDateFormat(strip
          (yearSymbols + monthSymbols + daySymbols, pattern));
      outFormat.setTimeZone(timeZone);
      return outFormat.format(d);
    } catch (ParseException pe) {
    }

    // Try the right truncated formats.
    for (int i = 0; i < formats.length; i++) {
      try {
        SimpleDateFormat inFormat = new SimpleDateFormat(formats[i]);
        inFormat.setLenient(false);
        Date d = inFormat.parse(dateTime);
        SimpleDateFormat outFormat = new SimpleDateFormat(pattern);
        outFormat.setTimeZone(timeZone);
        return outFormat.format(d);
      } catch (ParseException pe) {
      }
    }

    // Now try the left truncated ones. The Java format() function doesn't
    // return the correct strings in this case. We strip any pattern
    // symbols that shouldn't be output so that they are not defaulted to
    // inappropriate values in the output.
    try {
      SimpleDateFormat inFormat = new SimpleDateFormat(gmd);
      inFormat.setLenient(false);
      Date d = inFormat.parse(dateTime);
      SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
      outFormat.setTimeZone(timeZone);
      return outFormat.format(d);
    } catch (ParseException pe) {
    }
    try {
      SimpleDateFormat inFormat = new SimpleDateFormat(gm);
      inFormat.setLenient(false);
      Date d = inFormat.parse(dateTime);
      SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
      outFormat.setTimeZone(timeZone);
      return outFormat.format(d);
    } catch (ParseException pe) {
    }
    try {
      SimpleDateFormat inFormat = new SimpleDateFormat(gd);
      inFormat.setLenient(false);
      Date d = inFormat.parse(dateTime);
      SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols + monthSymbols, pattern));
      outFormat.setTimeZone(timeZone);
      return outFormat.format(d);
    } catch (ParseException pe) {
    }
    return EMPTY_STR;
  }

  /**
   * Strips occurrences of the given character from a date format pattern.
   *
   * @param symbols list of symbols to strip.
   */
  private static String strip(String symbols, String pattern) {
    int quoteSemaphore = 0;
    int i = 0;
    StringBuffer result = new StringBuffer(pattern.length());

    while (i < pattern.length()) {
      char ch = pattern.charAt(i);
      if (ch == '\'') {
        // Assume it's an openening quote so simply copy the quoted
        // text to the result. There is nothing to strip here.
        int endQuote = pattern.indexOf('\'', i + 1);
        if (endQuote == -1) {
          endQuote = pattern.length();
        }
        result.append(pattern.substring(i, endQuote));
        i = endQuote++;
      } else if (symbols.indexOf(ch) > -1) {
        // The char needs to be stripped.
        i++;
      } else {
        result.append(ch);
        i++;
      }
    }
    return result.toString();
  }

}
